﻿namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
    using System;
    using System.Diagnostics.Contracts;
    using System.Globalization;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    /// <summary>
    /// A utility class to make working with reflection easier.
    /// </summary>
    public static class ReflectionHelper
    {
        /// <summary>
        /// Creates a delegate for the specified method.
        /// </summary>
        /// <typeparam name="TDelegate">The type of the delegate.</typeparam>
        /// <param name="method">The method.</param>
        /// <returns>A delegate for the specified method.</returns>
        /// <remarks>
        /// The <see cref="Delegate.CreateDelegate"/> isn't generic so you have to specify the delegate type as
        /// well as casting the delegate to the correct delegate type. This saves specifying the type twice.
        /// </remarks>
        public static TDelegate CreateDelegate<TDelegate>(this MethodInfo method)
        {
            // NOTE: Cast to object first because C# won't let you cast a Delegate to something else.
            return (TDelegate)(object)Delegate.CreateDelegate(typeof(TDelegate), method);
        }

        /// <summary>
        /// Gets the field value.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="fieldName">Name of the field.</param>
        /// <returns>The field value.</returns>
        public static object GetFieldValue(object target, string fieldName)
        {
            FieldInfo fieldInfo = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            if (fieldInfo == null)
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "The field '{0}' could not be found in the type '{1}'.",
                        fieldName,
                        target.GetType().FullName));
            }

            return fieldInfo.GetValue(target);
        }

        /// <summary>
        /// Gets the field value.
        /// </summary>
        /// <param name="staticType">Type of the static that holds the field.</param>
        /// <param name="fieldName">Name of the field.</param>
        /// <returns>The field value.</returns>
        public static object GetFieldValue(this Type staticType, string fieldName)
        {
            FieldInfo fieldInfo = staticType.GetField(fieldName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

            if (fieldInfo == null)
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "The field '{0}' could not be found in the type '{1}'.",
                        fieldName,
                        staticType.FullName));
            }

            return fieldInfo.GetValue(null);
        }

        /// <summary>
        /// Gets the field value.
        /// </summary>
        /// <typeparam name="TTargetType">The type of the target.</typeparam>
        /// <param name="target">The target.</param>
        /// <param name="fieldName">Name of the field.</param>
        /// <returns>The field value.</returns>
        public static object GetFieldValue<TTargetType>(this TTargetType target, string fieldName)
        {
            FieldInfo fieldInfo = typeof(TTargetType).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            if (fieldInfo == null)
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "The field '{0}' could not be found in the type '{1}'.",
                        fieldName,
                        typeof(TTargetType).FullName));
            }

            return fieldInfo.GetValue(target);
        }

        /// <summary>
        /// Gets the <see cref="MethodInfo"/>.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <returns>The <see cref="MethodInfo"/>.</returns>
        public static MethodInfo GetMethodInfo(this Type type, string methodName)
        {
            return type.GetMethodInfo(methodName, Type.EmptyTypes);
        }

        /// <summary>
        /// Gets the <see cref="MethodInfo"/>.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="parameterTypes">The parameter types.</param>
        /// <returns>The <see cref="MethodInfo"/>.</returns>
        public static MethodInfo GetMethodInfo(this Type type, string methodName, params Type[] parameterTypes)
        {
            return type.GetMethod(
                methodName,
                BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
                null,
                parameterTypes,
                null);
        }

        /// <summary>
        /// Gets the property from the expression.
        /// </summary>
        /// <typeparam name="TType">The type to use..</typeparam>
        /// <param name="expression">The expression.</param>
        /// <returns>The property from the expression.</returns>
        public static PropertyInfo GetProperty<TType>(Expression<Func<TType, object>> expression)
        {
            MemberExpression memberExpression = expression.Body as MemberExpression;
            if (memberExpression.Member.MemberType == MemberTypes.Property)
            {
                return (PropertyInfo)memberExpression.Member;
            }

            throw new ArgumentException(
                string.Format("Couldn't get the property out of the expression: '{0}'.", expression.ToString()),
                "expression");
        }

        /// <summary>
        /// Gets the property value.
        /// </summary>
        /// <typeparam name="TTargetType">The type of the target.</typeparam>
        /// <typeparam name="TPropertyType">The type of the property.</typeparam>
        /// <param name="target">The target.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>The property value.</returns>
        public static TPropertyType GetPropertyValue<TTargetType, TPropertyType>(this TTargetType target, string propertyName)
        {
            Contract.Requires(propertyName != null);

            PropertyInfo propertyInfo = typeof(TTargetType).GetProperty(propertyName);
            return (TPropertyType)propertyInfo.GetValue(target, null);
        }

        /// <summary>
        /// Invokes the method.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns>The return value, if any, from the method.</returns>
        public static object InvokeMethod(this MethodInfo method, params object[] parameters)
        {
            if (method.IsStatic)
            {
                return method.Invoke(null, parameters);
            }
            else
            {
                return method.Invoke(parameters[0], parameters.Skip(1).ToArray());
            }
        }

        /// <summary>
        /// Invokes the method and unwrap the <see cref="TargetInvocationException"/> if it is thrown.
        /// </summary>
        /// <param name="method">The method to invoke.</param>
        /// <param name="parameters">The method parameters.</param>
        /// <returns>The return value if any.</returns>
        public static object InvokeMethodAndUnwrapTargetInvocationException(
            this MethodInfo method,
            params object[] parameters)
        {
            object returnValue = null;
            ExceptionHelper.RethrowInnerExceptionIfTargetInvocationException(
                () => returnValue = method.InvokeMethod(parameters));

            return returnValue;
        }

        /// <summary>
        /// Sets the field value.
        /// </summary>
        /// <typeparam name="TTargetType">The type of the target type.</typeparam>
        /// <param name="target">The target.</param>
        /// <param name="fieldName">Name of the field.</param>
        /// <param name="value">The value.</param>
        public static void SetFieldValue<TTargetType>(this TTargetType target, string fieldName, object value)
        {
            FieldInfo fieldInfo = typeof(TTargetType).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            if (fieldInfo == null)
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "The field '{0}' could not be found in the type '{1}'.",
                        fieldName,
                        typeof(TTargetType).FullName));
            }

            fieldInfo.SetValue(target, value);
        }
    }
}